﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Sustainalytics.BusinessResearchProductStates.Entities.ResearchState;
using Sustainalytics.MongoDB.Extensions;
using Sustainalytics.Synchronization.Interface;
using Sustainalytics.Utils;
using Sustainalytics.Utils.Configuration;
using Sustainalytics.ESG.Entities;
using CorporateData = Sustainalytics.ClientDW.Entities.CorporateData;

namespace Sustainalytics.CRT.Synchronization
{
    public class FocusedRatingsKeyEsgIssuesManager
    {
        private List<CorporateData> companyProfiles;
        private Dictionary<Guid, List<RPIndicatorExtended>> indicatorsDictionary;
        private Dictionary<KeyValuePair<string, string>, Tuple<float, float>> groupIndicatorsDictionary;
        private Dictionary<Guid, List<RPIndicatorExtended>> controversyIndicatorsDictionary;
        private Dictionary<KeyValuePair<string, string>, Tuple<float, float>> groupControversyIndicatorsDictionary;

        public void Sync(SyncContext syncContext)
        {
            var themes = new List<string> { "Environment", "Social", "Governance" };

            var companyIdsWithCoreResearch = MongoFactory.GetCollectionFromDatabase<CompanyProfileResearchState>
                (syncContext.GAConnectionStrings["ClientDW"])
                .Find(cd => cd.CoreEsgResearchState.ResearchState == ResearchState.ResearchAvailable)
                .Project(cd => cd.Id).ToListAsync().Result;

            var companyProfilesWithComprehensiveResearch = MongoFactory.GetCollectionFromDatabase<CompanyProfileResearchState>
                 (syncContext.GAConnectionStrings["ClientDW"])
                 .Find(cd => cd.ComprehensiveEsgResearchState.ResearchState == ResearchState.ResearchAvailable)
                 .Project(cd => cd.Id).ToListAsync().Result;

            //in case a profile appears accidentally as both core and comprehensive, the latter should prevail
            companyIdsWithCoreResearch = companyIdsWithCoreResearch.Except(companyProfilesWithComprehensiveResearch).ToList();

            new Action[]
            {
                () => companyProfiles = GetCompanyProfiles(syncContext, companyIdsWithCoreResearch),
                () => indicatorsDictionary = GetIndicators(syncContext, companyIdsWithCoreResearch),
                () => groupIndicatorsDictionary = GetGroupIndicators(syncContext,companyProfilesWithComprehensiveResearch),
                () => controversyIndicatorsDictionary = GetControversyIndicators(syncContext, companyIdsWithCoreResearch),
                () => groupControversyIndicatorsDictionary = GetGroupControversyIndicators(syncContext, companyProfilesWithComprehensiveResearch)
            }
            .RunInParallel(syncContext.Token);

            var companyKeyESGIssues = new ConcurrentBag<CompanyKeyESGIssue>();

            var exceptions = new ConcurrentQueue<System.Exception>();

            Parallel.ForEach(companyProfiles,
                new ParallelOptions()
                {
                    MaxDegreeOfParallelism = SettingsHelper.Read("MaxDegreeOfParallelism", 1)
                },
                cp =>
                {
                    try
                    {
                        List<RPIndicatorExtended> indicators;
                        List<RPIndicatorExtended> controversyIndicators;

                        if (!indicatorsDictionary.TryGetValue(cp.Id, out indicators))
                        {
                            indicators = new List<RPIndicatorExtended>(0);
                        }

                        if (!controversyIndicatorsDictionary.TryGetValue(cp.Id, out controversyIndicators))
                        {
                            controversyIndicators = new List<RPIndicatorExtended>(0);
                        }

                        var companyKeyESGIssue = new CompanyKeyESGIssue
                        {
                            Id = cp.Id,
                            CompanyName = cp.CompanyName,
                            SubPeerGroupId = cp.SubPeerGroupId
                        };

                        if (syncContext.CompanyUniverses != null)
                        {
                            companyKeyESGIssue.UniverseIds = syncContext.CompanyUniverses.ContainsKey(cp.Id)
                                ? syncContext.CompanyUniverses[cp.Id]
                                : null;
                        }

                        companyKeyESGIssue.KeyESGIssues = new List<KeyEsgIssue>();

                        foreach (var theme in themes)
                        {
                            var keyEsgIssue = new KeyEsgIssue()
                            {
                                Name = theme,
                                KeyEsgIssueIndicators = new List<KeyEsgIssueIndicator>()
                            };

                            companyKeyESGIssue.KeyESGIssues.Add(keyEsgIssue);


                            if (indicators.Any())
                            {
                                var themeName = theme;
                                var themeIndicators = indicators.Where(i => i.Template.ThemeName == themeName).ToList();

                                if (themeIndicators.Any())
                                {
                                    themeIndicators.ForEach(i =>
                                    {
                                        Tuple<float, float> aggregates;

                                        if (!groupIndicatorsDictionary.TryGetValue(
                                                new KeyValuePair<string, string>(cp.PeerGroup, i.Template.Number),
                                                out aggregates))
                                        {
                                            aggregates = new Tuple<float, float>(0, 0);
                                        }

                                        keyEsgIssue.KeyEsgIssueIndicators.Add(new KeyEsgIssueIndicator
                                        {
                                            Type = KeyEsgIssueIndicatorType.StandardIndicator,
                                            Number = i.Template.Number,
                                            Name = i.Template.Name,
                                            Score = i.SelectedAnswer.Score.ToString(),
                                            Average = $"{aggregates.Item1}",
                                            BestPractice = $"{aggregates.Item2}",
                                            MaterialBestPracticeGap =
                                                GetMaterialBestPracticeGap(i.SelectedAnswer.Score, aggregates.Item2)
                                        });
                                    });
                                }
                            }

                            if (controversyIndicators.Any())
                            {
                                var themeName = theme;
                                var themeControversyIndicators = controversyIndicators.Where(i => i.Template.ThemeName == themeName).ToList();

                                if (themeControversyIndicators.Any())
                                {
                                    themeControversyIndicators.ForEach(ci =>
                                    {
                                        Tuple<float, float> aggregates;

                                        if (
                                            !groupControversyIndicatorsDictionary.TryGetValue(
                                                new KeyValuePair<string, string>(cp.PeerGroup, ci.Template.Number),
                                                out aggregates))
                                            aggregates = new Tuple<float, float>(0, 0);

                                        keyEsgIssue.KeyEsgIssueIndicators.Add(new KeyEsgIssueIndicator
                                        {
                                            Type = KeyEsgIssueIndicatorType.StandardIndicator,
                                            IsControversyIndicator = true,
                                            Number = ci.Template.Number,
                                            Name = ci.Template.Name,
                                            Score = ci.SelectedAnswer.Score.ToString(),
                                            Average = $"{aggregates.Item1}",
                                            BestPractice = $"{aggregates.Item2}",
                                            MaterialBestPracticeGap =
                                                GetMaterialBestPracticeGap(ci.SelectedAnswer.Score, aggregates.Item2)
                                        });
                                    });
                                }
                            }
                        }

                        companyKeyESGIssues.Add(companyKeyESGIssue);
                    }
                    catch (System.Exception ex)
                    {
                        exceptions.Enqueue(ex);
                    }
                });

            if (exceptions.Count > 0)
            {
                throw new AggregateException(exceptions);
            }

            SaveCompanyKeyESGIssues(syncContext, companyKeyESGIssues.ToList(), companyProfilesWithComprehensiveResearch);
        }

        private void SaveCompanyKeyESGIssues(SyncContext syncContext, List<CompanyKeyESGIssue> companyKeyESGIssues, List<Guid> companyProfilesWithComprehensiveResearch)
        {
            if (companyKeyESGIssues == null || !companyKeyESGIssues.Any())
            {
                return;

            }

            var collection = MongoFactory.GetCollectionFromDatabase<CompanyKeyESGIssue>(syncContext.GAConnectionStrings["ClientDW"]);

            collection.DeleteMany(c => !companyProfilesWithComprehensiveResearch.Contains(c.Id));

            collection.InsertMany(companyKeyESGIssues);
        }

        private static int GetMaterialBestPracticeGap(float score, float bestPractice)
        {
            var gap = 0;
            if (score >= 0 && bestPractice >= 0)
            {
                var diff = bestPractice - score; // as required by Andreea Aldea

                if (diff < 25)
                {
                    gap = 1;
                }
                else if (diff < 75)
                {
                    gap = 2;
                }
                else
                {
                    gap = 3;
                }
            }

            return gap;
        }

        private static List<CorporateData> GetCompanyProfiles(SyncContext syncContext, List<Guid> companyIds)
        {
            return
                MongoFactory.GetCollectionFromDatabase<CorporateData>(syncContext.GAConnectionStrings["ClientDW"])
                    .Find(cp => companyIds.Contains(cp.Id))
                    .Project(cd => new CorporateData
                    {
                        Id = cd.Id,
                        CompanyName = cd.CompanyName,
                        PeerGroup = cd.PeerGroup,
                        SustainalyticsSubIndustry = cd.SustainalyticsSubIndustry,
                        SubPeerGroupId = cd.SubPeerGroupId
                    })
                    .ToListAsync().Result;

        }

        private static Dictionary<Guid, List<RPIndicatorExtended>> GetControversyIndicators(SyncContext syncContext, List<Guid> companyIds)
        {
            var controversyIndicators = MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(syncContext.GAConnectionStrings["ClientDW"])
                .Find(ci => companyIds.Contains(ci.CompanyId) && ci.Template.IsControversyIndicator)
                .ToListAsync().Result;
            return controversyIndicators
               .GroupBy(ci => ci.CompanyId)
               .ToDictionary(g => g.Key, g => g.ToList());
        }

        private static Dictionary<KeyValuePair<string, string>, Tuple<float, float>> GetGroupControversyIndicators(SyncContext syncContext, List<Guid> companyIdsWithComprehensiveResearch)
        {
            var controversyIndicatorNumbers = new List<string>
            {
                "G.1.5",
                "G.2.13",
                "G.3.4",
                "S.1.7",
                "S.2.3",
                "S.3.3",
                "S.4.3",
                "E.1.12",
                "E.2.2",
                "E.3.2"
            };

            var companies = MongoFactory.GetCollectionFromDatabase<CorporateData>(syncContext.GAConnectionStrings["ClientDW"])
                .Find(cp => companyIdsWithComprehensiveResearch.Contains(cp.Id))
                .Project(cd => new
                {
                    cd.Id,
                    cd.PeerGroup
                }).ToListAsync().Result;

            var peerGroups = companies.Where(c => c.PeerGroup != null).GroupBy(c => c.PeerGroup)
                .ToDictionary(g => g.Key, g => g.Select(x => x.Id).ToList());

            var dictionary = new ConcurrentDictionary<KeyValuePair<string, string>, Tuple<float, float>>();

            Parallel.ForEach(peerGroups,
                new ParallelOptions()
                {
                    MaxDegreeOfParallelism = SettingsHelper.Read("MaxDegreeOfParallelism", 1)
                },
                peerGroup =>
                {
                    controversyIndicatorNumbers.ForEach(number =>
                    {
                        var ids = peerGroup.Value;

                        var controversyIndicators =
                             MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(syncContext.GAConnectionStrings["ClientDW"])

                                .Find(ci => ids.Contains(ci.CompanyId) && ci.Weight.Value.HasValue && ci.Template.Number == number)
                                .ToListAsync().Result;

                        float pgAverage = 0;
                        float bestPractice = 0;

                        if (controversyIndicators.Any())
                        {
                            pgAverage = Convert.ToInt32(controversyIndicators.Average(ci => ci.SelectedAnswer.Score));

                            var take = Convert.ToInt32(Math.Ceiling(controversyIndicators.Count / 4.0f));

                            bestPractice = Convert.ToInt32(controversyIndicators.OrderByDescending(ei => ei.SelectedAnswer.Score.Value)
                                .Take(take)
                                .Average(ei => ei.SelectedAnswer.Score));
                        }

                        dictionary.TryAdd(new KeyValuePair<string, string>(peerGroup.Key, number), new Tuple<float, float>(pgAverage, bestPractice));
                    });
                });

            return dictionary.ToDictionary(d => d.Key, d => d.Value);
        }

        private static Dictionary<KeyValuePair<string, string>, Tuple<float, float>> GetGroupIndicators(SyncContext syncContext, IList<Guid> comprehensiveCompanyIds)
        {
            var companies = MongoFactory.GetCollectionFromDatabase<CorporateData>(syncContext.GAConnectionStrings["ClientDW"])
                .Find(cp => comprehensiveCompanyIds.Contains(cp.Id))
                .Project(cd => new
                {
                    cd.Id,
                    cd.PeerGroup
                }).ToListAsync().Result;


            var peerGroups = companies.Where(c => c.PeerGroup != null).GroupBy(c => c.PeerGroup)
                .ToDictionary(g => g.Key, g => g.Select(x => x.Id).ToList());

            var builder = Builders<RPIndicatorExtended>.Filter;
            var filter = builder.Where(i => i.Template.IsControversyIndicator == false) &
                         builder.In(x => x.CompanyId, comprehensiveCompanyIds);

            var indicatorsList = MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(syncContext.GAConnectionStrings["ClientDW"])
                .Find(filter).Project(x => x.Template.Number).ToListAsync().Result;

            var indicatorNumbers = indicatorsList.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList();

            var dictionary = new ConcurrentDictionary<KeyValuePair<string, string>, Tuple<float, float>>();

            Parallel.ForEach(peerGroups,
                new ParallelOptions()
                {
                    MaxDegreeOfParallelism = SettingsHelper.Read("MaxDegreeOfParallelism", 1)
                },
                peerGroup =>
                {
                    indicatorNumbers.ForEach(number =>
                   {
                       var ids = peerGroup.Value;

                       var indicators = MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(syncContext.GAConnectionStrings["ClientDW"])
                           .Find(i => ids.Contains(i.CompanyId) && i.Weight.Value.HasValue && i.Template.Number == number)
                           .ToListAsync().Result;

                       float peerGroupAverage = 0;
                       float bestPractice = 0;

                       if (indicators.Any())
                       {
                           var average = indicators.Average(i => i.SelectedAnswer.Score);
                           peerGroupAverage = (float)Math.Round(average, MidpointRounding.AwayFromZero);

                           var take = Convert.ToInt32(Math.Ceiling(indicators.Count / 4.0f));

                           var partialAverage = indicators.OrderByDescending(i => i.SelectedAnswer.Score.Value)
                               .Take(take)
                               .Average(i => i.SelectedAnswer.Score);

                           bestPractice = (float)Math.Round(partialAverage, MidpointRounding.AwayFromZero);
                       }

                       dictionary.TryAdd(new KeyValuePair<string, string>(peerGroup.Key, number), new Tuple<float, float>(peerGroupAverage, bestPractice));
                   });
                });

            return dictionary.ToDictionary(d => d.Key, d => d.Value);
        }

        private static Dictionary<Guid, List<RPIndicatorExtended>> GetIndicators(SyncContext syncContext, List<Guid> companyIds)
        {
            var indicators = MongoFactory.GetCollectionFromDatabase<RPIndicatorExtended>(
                syncContext.GAConnectionStrings["ClientDW"])
                .Find(i => !i.Template.IsControversyIndicator && companyIds.Contains(i.CompanyId))
                .ToListAsync().Result;

            return indicators
               .GroupBy(i => i.CompanyId)
               .ToDictionary(g => g.Key, g => g.ToList());
        }
    }
}
